/**
 * 
 */
package com.devmindset.jpatoolbox;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import org.apache.log4j.Logger;

/**
 * <pre>
 * Utility class to wire the bidirectionality of a Many to Many relationship.
 * 
 * The wiring is done according to the following rules and conventions:
 * 
 * Use case: Customer <-> Service.
 * 
 * For this relationship, we can say that one customer can use multiple services
 * and one service can refer to multiple customers.
 * 
 * Also, from the business logic perspective, it makes sense to have relationship
 * from the customer to the service and vice versa, hence the relationship is
 * classified as bidirectional.
 * 
 * We assume the following:
 * 
 * Both sides represented by collections have set, add and remove
 * accessor methods. For example:
 * 
 * <code>Customer</code> has <code>setServices(List<Service> services)</code>,
 * <code>addService(Service service)</code> and <code>removeService(Service service)</code>
 * methods by convention.
 * 
 * Similarly, by convention, the <code>Service</code> entity has the <code>setCustomers(List<Customer> customers)</code>,
 * <code>addCustomer(Customer customer)</code> and <code>removeCustomer(Customer customer)</code> methods by convention.
 * 
 * To wire the sides you use the methods of this class; see the specific methods for usage details.
 * </pre>
 * 
 * @author <a href="mailto:ice.penov@devmindset.com">Ice Penov</a>
 */
@SuppressWarnings("unchecked")
public final class ManyToManyUtil {
  
  private static final Logger logger = Logger.getLogger(ManyToManyUtil.class);
  
  private ManyToManyUtil() {
  }
  
  /**
   * <pre>
   * Wires the Many to Many bidirectional relationship, by means of association.
   * 
   * For the Customer <-> Service use case, you use this method in the setter on
   * each one of the M side, in this case:
   * 
   * In the Customer class:
   * 
   * public void setServices(List<Service> services) {
   *   ManyToManyUtil.wireCollection(this, services, Customer.class, Service.class);
   * }
   * 
   * Similarly, on the other M side (in the Service class):
   * 
   * public void setCustomers(List<Customer> customers) {
   *   ManyToManyUtil.wireCollection(this, customers, Service.class, Customer.class);
   * } 
   * </pre>
   * 
   * @param thisSide this side of the M to M relationship
   * @param otherSide the other side of the M to M relationship
   * @param thisSideClass the class on this side of the relationship
   * @param otherSideClass the class on the other side of the relationship
   */
  public static void wireCollection(Object thisSide, List otherSide, Class thisSideClass, Class otherSideClass) {
    try {
      Constructor arrayListConstructor = ArrayList.class.getConstructor(Collection.class);
      Method remove = List.class.getMethod("remove", Object.class);
      Field otherSideField = thisSideClass.getDeclaredField(Composer.composeCollectionFieldName(otherSideClass));
      otherSideField.setAccessible(true);
      Method removeThisSide = otherSideClass.getMethod(Composer.composeRemover(thisSideClass), thisSideClass);
      Method addOtherSide = thisSideClass.getMethod(Composer.composeAdder(otherSideClass), otherSideClass);
      
      List copyList = (List) arrayListConstructor.newInstance(otherSideField.get(thisSide));
      for (Object object : copyList) {
        remove.invoke(otherSideField.get(thisSide), object);
        removeThisSide.invoke(object, thisSide);
      }
      for (Object object : otherSide) {
        addOtherSide.invoke(thisSide, object);
      }
    } catch (Exception e) {
      logger.error("Error while wiring collection on a M to M bidirectional relationship", e);
    }    
  }
  
  /**
   * <pre>
   * Wires the Many to Many bidirectional relationship, by means of addition.
   * 
   * For the Customer <-> Service use case, you use this method in the adder on
   * each one of the M side, in this case:
   * 
   * In the Customer.class:
   * 
   * public void addService(Service service) {
   *   ManyToMany.wireAddition(this, service, Customer.class, Service.class);
   * }
   * 
   * Similarly, on the other M side (in the Service class):
   * 
   * public void addService(Service service) {
   *   ManyToMany.wireAddition(this, service, Service.class, Customer.class);
   * }
   * </pre>
   * 
   * @param thisSide this side of the M to M relationship
   * @param otherSide the other side of the M to M relationship
   * @param thisSideClass the class on this side of the relationship
   * @param otherSideClass the class on the other side of the relationship
   */
  public static void wireAddition(Object thisSide, Object otherSide, Class thisSideClass, Class otherSideClass) {
    try {
      Field otherSideField = thisSideClass.getDeclaredField(Composer.composeCollectionFieldName(otherSideClass));
      otherSideField.setAccessible(true);
      Method contains = List.class.getMethod("contains", Object.class);
      Method add = List.class.getMethod("add", Object.class);
      Method addThisSide = otherSideClass.getMethod(Composer.composeAdder(thisSideClass), thisSideClass);
      
      Boolean b = (Boolean) contains.invoke(otherSideField.get(thisSide), otherSide);
      if (!b) {
        add.invoke(otherSideField.get(thisSide), otherSide);
        addThisSide.invoke(otherSide, thisSide);
      }
    } catch (Exception e) {
      logger.error("Error while wiring additition on a M to M bidirectional relationship", e);
    }
  }
  
  /**
   * <pre>
   * Wires the Many to Many bidirectional relationship, by means of removal.
   * 
   * For the Customer <-> Service use case, you use this method in the removal on
   * each one of the M side, in this case:
   * 
   * In the Customer class:
   * 
   * public void removeService(Service service) {
   *   ManyToMany.wireRemoval(this, service, Customer.class, Service.class); 
   * }
   * 
   * Similarly, on the other M side (in the Service class):
   * 
   * public void removeCustomer(Customer customer) {
   *   ManyToMany.wireRemoval(this, customer, Service.class, Customer.class);
   * }
   * </pre>
   * 
   * @param thisSide this side of the M to M relationship
   * @param otherSide the other side of the M to M relationship
   * @param thisSideClass the class on this side of the relationship
   * @param otherSideClass the class on the other side of the relationship
   */
  public static void wireRemoval(Object thisSide, Object otherSide, Class thisSideClass, Class otherSideClass) {
    try {
      Field otherSideField = thisSideClass.getDeclaredField(Composer.composeCollectionFieldName(otherSideClass));
      otherSideField.setAccessible(true);
      Method contains = List.class.getMethod("contains", Object.class);
      Method remove = List.class.getMethod("remove", Object.class);
      Method removeThisSide = otherSideClass.getMethod(Composer.composeRemover(thisSideClass), thisSideClass);
      
      if ((Boolean) contains.invoke(otherSideField.get(thisSide), otherSide)) {
        remove.invoke(otherSideField.get(thisSide), otherSide);
        removeThisSide.invoke(otherSide, thisSide);
      }
    } catch (Exception e) {
      logger.error("Error while wiring removal on a M to M bidirectional relationship", e);
    }
  }

}
